Unity 入门精要学习笔记 第十一单元 让画面动起来

您所在的位置:网站首页 unity shader lerp Unity 入门精要学习笔记 第十一单元 让画面动起来

Unity 入门精要学习笔记 第十一单元 让画面动起来

2023-03-05 00:19| 来源: 网络整理| 查看: 265

10.1 Unity 内置的时间变量

Image

11.2 纹理动画

纹理动画在游戏中的应用非常广泛,尤其是各种资源都比较局限的移动平台,我们往往会使用纹理动画来代替开销巨大的粒子系统模拟的各种动画效果。

11.2.1 序列帧动画

序列帧动画即播放一系列的关键帧图像,不需要进行任何物理计算就可以得到精美的效果,但是代价是美术工程量巨大。

序列帧图像通常是透明纹理,所以需要设置它的队列。

由于天空盒的渲染在透明物体之后,而对于透明度混合的物体来说,需要关闭深度写入。所以在渲染天空盒时,如果透明物体后没有不透明物体,那该物体对应的 z-buffer 为空,Unity 就会将天空盒的颜色直接写入颜色缓冲,那么在 摄像机或是Game视图 透明物体就不会显示,所以观察透明物体的效果时,需要关闭天空盒 或是在透明物体后加 plane 等不透明物体。

下面这张图就非常明显,左边是透明度混合的立方体,没有不透明物体的地方直接不显示,右边是 透明度测试的立方体就没问题(因为开启了深度写入)

Image

场景搭建:在场景中创建一个四边形(quad),调整它的正面朝向摄像机(背面会被裁剪掉),并为其创建对应的材质和 shader。

Shader:注意不要忘了 Pass 中属性的声明 以及 顶点着色器的输入输出结构体的定义(1)声明材质的各种属性

Properties { _Color ("Color Tint", Color) = (1, 1, 1, 1) _MainTex ("Texture", 2D) = "white" {} _HorizontalAmount ("Horizontal Amount", Float) = 8 _VerticalAmount ("Vertical", Float) = 8 _Speed ("Speed", Range(1, 100)) = 30 }

其中 _MainTex 用于存储序列帧纹理,_HorizontalAmount 和 _VerticalAmount 分别对应水平和竖直方向上序列帧个数,例如一个 5 * 6 的序列帧纹理,上述变量的值分别为 5 和 6 。而 _Speed 用于控制播放速度。

(2)由于序列帧动画通常是透明纹理,所以需要为其设置 Pass 的相关状态。

SubShader { Tags { "Quene" = "Transparent" "IgnoreProjector" = "True" "RenderType" = "Transparent"} Pass { Tags {"LightMode" = "ForwardBase"} ZWrite Off Blend SrcAlpha OneMinusSrcAlpha

(4)顶点着色器:

v2f vert(appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = TRANSFORM_TEX(v.uv, _MainTex); return o; }

(5)片元着色器

fixed4 frag(v2f i) : SV_Target { float time = floor(_Time.y * _Speed); float row = floor(time / _HorizontalAmount); float col = time - row * _HorizontalAmount; half2 uv = i.uv + half2(col, -row); uv.x /= _HorizontalAmount; uv.y /= _VerticalAmount; fixed4 c = tex2D(_MainTex, uv); c *= _Color; return c; }

通过 _Time.y * _Speed 控制图像切换速度,并求出改时间下应当播放的图像所在的行和列。然后把某一帧图像所对应的纹理的 x,y映射的到 [0,1],就拿该代码中的 8*8 的序列帧纹理为例,它的第一个图像的x、y 的范围都是 [0, 0.125],需要将其映射到 [0, 1]。需要注意 Unity 纹理,是从下到上的,而序列帧是从上到下的,所以进行偏移时 v 方向需要减去 row。(如果将图像的长和宽看成单位长度的话,那最右上角的坐标就是(_VerticalAmount,_HorizontalAmount),那只需要将改坐标系下的坐标除以整个纹理的长和宽就可以映射到 [0, 1])

(5) 设置 Fallback

Fallback "Transparent/VertexLit" 11.2.2 滚动背景

远近两层背景,仅距离背景滚动快,远距离背景滚动慢。

场景搭建:第一步关闭天空盒,然后将摄像机的模式设置为 正交投影,创建一个四边形(quad),并将其铺满正交区域,注意需正面朝向摄像机。

创建对应的材质和 Shader,并将其赋给 四边形。

Shader:

(1)声明属性:

Properties { _MainTex("Base Layer (RGB)", 2D) = "white" {} _DetailTex("2nd Layer (RGB)", 2D) = "white" {} //远平面背景滚动速度 _ScrollX("Base Tex Scroll Speed", Float) = 1.0 //近平面背景滚动速度 _Scroll2X("2nd Tex Scroll Speed", Float) = 1.0 //控制图像亮度 _Multiplier("Layer Multiplier", Float) = 1.0 }

(2)顶点着色器:

v2f vert(appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv.xy = TRANSFORM_TEX(v.uv, _MainTex) + frac(float2(_ScrollX, 0) * _Time.y); o.uv.zw = TRANSFORM_TEX(v.uv, _DetailTex) + frac(float2(_Scroll2X, 0) * _Time.y); return o; }

想让图像在水平方向滚动,例如向左滚动,就要让同一个顶点在随着时间的递增,其对应的纹理坐标不断向左移动,也就是 o.uv + (offset, 0)。其中 frac 函数的作用是 给一个向量,返回对应该向量每个分量的小数部分。frac 函数介绍。

(3)片元着色器:

fixed4 frag(v2f i) : SV_Target { fixed4 firstLayer = tex2D(_MainTex, i.uv.xy); fixed4 secondLayer = tex2D(_DetailTex, i.uv.zw); fixed4 c = lerp(firstLayer, secondLayer, secondLayer.a); c.rgb *= _Multiplier; return c; }

用 secondLayer 也就是近平面的 alpha 分量来作为 lerp 的第三个参数,是因为想让近平面覆盖远平面。

(4)设置 Fallback,也可以关闭 Fallback

Fallback "VertexLit" 11.3 顶点动画11.3.1 流动的河流

用正弦函数对模型的顶点进行上下偏移,来模拟河流的波动。用时间水平方向上的纹理采样,来模拟河流的流动。

Image

场景搭建:

我们从首先取消天空盒,然后从 Prefabs 文件夹中拖出 Water 模型,默认情况下它的方向可能有些问题,可能会背面朝向我们,就不会显示,可以将其向上的轴取相反数。(我是吧 Y: -90 改成了 90)。然后再拖出两个,并按如图放置。然后创建 3 个材质 和 一个 Shader。

这是我们随机点一个 water,如下图,我们发现改模型 x 轴来到了竖直方向,z 来到了 水平方向(红绿蓝轴 依次对应 xyz 轴),所以写代码时,需要用对 x 分量进行偏移,模拟波动,z 方向用时间控制纹理偏移,来实现流动效果。

Image

Shader:

(1)声明一些属性

Properties { _Color("Color Tint", Color) = (1, 1, 1, 1) _MainTex("Texture", 2D) = "white" {} //控制水流波动的幅度,0.1 似乎比较合适 _Magnitude("Distortion Magnitude", Float) = 1 //控制水流波动频率 _Frequency("Distortion Frequency", Float) = 1 //控制波长的倒数 _InvWaveLength("Distortion Inverse Wave Length", Float) = 1 _Speed("Speed", Float) = 1 }

(2)为透明效果设置合适的 SubShader 标签:

SubShader { Tags { "RenderType" = "Transparent" "IngoreProjector" = "True" "Quene" = "Transparent" "DisableBatching"="True"}

在上面的设置中,我们除了为透明效果设置Qucue、IgnoreProjector 和 RendeaType外,还设量了一个新的标签——DisableBatching。我们在3.3.3 节中介绍过该标签的含义:一些 SubShader 在使用Unity 的批处理功能时会出现问题,这时可以通过该标签来直接指明是否对该 SubShader 使用批处理。而这些需要特殊处理的 Shader 通常就是指包含了模型空间的顶点动画的Shadr. 这是因为批处理会合并所有相关的模型,而这些模型各自的模型空间就会丢失。而在本例中,我们需要在物体的模型空间下对顶点位置进行偏移。因此,在这里需要取消对该 Shader的批处理操作。

(3)设置 Pass 的渲染状态

Pass { Tags {"LightMode" = "ForwardBase"} ZWrite Off Blend SrcAlpha OneMinusSrcAlpha Cull off

(4)顶点着色器

v2f vert(appdata v) { v2f o; float4 offset; offset.yzw = float3(0.0, 0.0, 0.0); offset.x = sin(_Frequency * _Time.y + dot(v.vertex, float3(_InvWaveLength, _InvWaveLength, _InvWaveLength))) * _Magnitude; o.vertex = UnityObjectToClipPos(v.vertex + offset); o.uv = TRANSFORM_TEX(v.uv, _MainTex); o.uv += float2(0, _Time.y * _Speed); return o; }

用 _Frequency 属性和内置的 _Time.y 来控制正弦函数的频率。同时为了让不同位置具有不同的位移,我们对上述结果加上了模型空间下的位置分量,并乘以 _InWaveLength 来控制波长。最后乘上 _Magnitude 控制振幅。最后用 _Time.y 和 _speed 来控制水平方向上的纹理动画。

(5)片元着色器:对纹理采样,并添加颜色控制即可:

fixed4 frag(v2f i) : SV_Target { fixed4 c = tex2D(_MainTex, i.uv); c.rgb *= _Color.rgb; return c; }

(6)设置 Fallback

Fallback "Transparent/VertexLit" 11.3.2 广告牌

**广告牌技术(Billboarding)**会根据视角方向来渲染一个被纹理着色的多边形(通常是四边形),这使得多边形好像总是面对着摄像机。广告牌技术用应用于渲染烟雾、云朵、闪光效果 的插片上。

该技术本质就是构建旋转矩阵,矩阵需要三个基向量:表面法线(normal)、指向上的方向(up),指向右的方向(right),除此之外还需指定一个锚点(anchor location) ,可以用模型的正中心(模型空间的坐标(0, 0, 0))。

难点也在于构建旋转矩阵,一般我们会通过初始计算得出表面法线(例如视角方向,因为我们希望模型始终面朝我们)和指向上的方向,这两者往往不是垂直的。但是这两者其中之一是固定的,例如模拟草丛时,我们希望其指向上的方向永远是 (0, 1, 0),而法线随视角变化;而模拟粒子效果时(例如烟雾的插片),我们希望法线始终是视线方向,而指向上的方向是可以改变的。

我们假设法线固定,首先可以根据法线和想向上的方向的叉积求出向右的方向(可以将right、up、normal 当成 x,y,z 轴):$$right=up × normal$$归一化后,再有法线方向与向右方向计算出正交的向上的方向:$$up' = normal × right$$

Image

搭建场景:先去掉场景中的天空盒,然后创建 材质和 Shader,然后再场景中,创建多个四边形(并调整他们的位置和大小),并将创建的材质赋给它们。

Image

Shader:

(1)声明属性:

Properties { _Color("Color Tint", Color) = (1, 1, 1, 1) _MainTex("Texture", 2D) = "white" {} //调整时固定法线还是固定指向上方向,即约束垂直方向的程度 _VerticalBillboarding("Vertical Restraints", Range(0, 1)) = 1 }

其中通过将 _VerticalBillboarding 与 法线的 y 分量相乘,如果 _VerticalBillboarding 为 0, 那乘完后法线的 y 分量为0, 如果向上的方向要与其垂直,那么也需要指向上方,从而达到控制固定指向上方向的目的。

(2)为透明效果设置合适的标签:

SubShader { //由于是顶点动画所以需要解绑定 Tags { "RenderType" = "Transparent" "IngoreProjector" = "True" "Quene" = "Transparent" "DisableBatching" = "True"}

(3)设置 Pass 的渲染状态:

Pass { Tags{"LightMode" = "ForwardBase"} ZWrite Off Blend SrcAlpha OneMinusSrcAlpha Cull Off

关闭剔除功能是为了让每一面都能显示。

(4)顶点着色器是我们的核心,所有计算都是在模型空间下计算,我们选择模型空间的原点,即模型的中心做描点:

v2f vert(appdata v) { v2f o; float3 center = float3(0, 0, 0); float3 viewer = mul(unity_WorldToObject, float4(_WorldSpaceCameraPos, 1)); fixed3 normalDir = viewer - center; normalDir.y *= _VerticalBillboarding; normalDir = normalize(normalDir); //为了防止法线与向上的方向平行,当法线的y分量接近 1 时, //我们将向上的方向设置为 (0,0,1), 否则叉积会得到错误的结果, float3 upDir = abs(normalDir.y) > 0.999 ? float3(0, 0, 1) : float3(0, 1, 0); fixed3 rightDir = normalize(cross(upDir, normalDir)); upDir = normalize(cross(normalDir, rightDir)); float3 centerOffset = v.vertex.xyz - center; //这一步向相当于左乘旋转矩阵,矩阵的第 n 列乘向量的第 n 行,并相乘。 float3 localPos = center + centerOffset.x * rightDir + centerOffset.y * upDir + centerOffset.z * normalDir; o.vertex = UnityObjectToClipPos(float4(localPos, 1)); o.uv = TRANSFORM_TEX(v.uv, _MainTex); return o; }

(5)片元着色器:

fixed4 frag(v2f i) : SV_Target { // sample the texture fixed4 c = tex2D(_MainTex, i.uv); c.rgb *= _Color.rgb; return c; }

(6)设置 Fallback

Fallback "Transparent/VertexLit"

其他需要注意的是,我们使用的是 Unity 自带的四边形(Quad),而不是平面(plane),这是因为我们的代码是建立在一个竖直摆放的多边形的基础上的,也就是说这个多边形的顶点结构必须满足在模型空间下是竖直排列的。只有这样才能计算出正确的相对于锚点的偏移量。

由于是透明的物体,所以 Fallback 用的是 Transparent/VertexLit ,但是它无法投射阴影,如果想要其投射阴影,需要将其换成 VertexLit,但是该中放发投射的阴影不会产生相应的动画效果:

Image

我们可以写一个专门用于它的 ShadowCaster Pass,来投射对应动画顶点阴影, 来代替 Fallback

Pass { Tags{ "LightMode"="ShadowCaster" } CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile_shadowcaster #include "UnityCG.cginc" float _Magnitude; float _Frequency; float _InvWaveLength; float _Speed; struct a2v { float4 vertex : POSITION; float4 normal : NORMAL; }; struct v2f { V2F_SHADOW_CASTER; }; v2f vert(a2v v) { v2f o; float4 offset; offset.yzw = float3(0, 0, 0); offset.x = sin(_Frequency * _Time.y + dot(v.vertex.xyz, float3(_InvWaveLength, _InvWaveLength, _InvWaveLength))) * _Magnitude; v.vertex = v.vertex + offset; TRANSFER_SHADOW_CASTER_NORMALOFFSET(o); return o; } fixed4 frag(v2f i) : SV_Target { SHADOW_CASTER_FRAGMENT(i) } ENDCG }

Image

我们通常使用 Unity 提供的内置宏 V2F_SHADOW_CASTER、TRANSFORM_SHADOW_CASTER_NORMALOFFSET、SHADOW_CASTER_FRAGMENT 来计算阴影投射所需各种变量,而我们可以只关心自定义的部分。在上述代码中我们首先在 v2f 结构体中利用 V2F_SHADOW_CASTER,来定义阴影投射所需的各种变量,然后计算顶点的偏移,并加到顶点位置变量中,然后使用 TRANSFORM_SHADOW_CASTER_NORMALOFFSET 让 Unity 帮我们完成剩下的事情。在片元着色器中用 SHADOW_CASTER_FRAGMENT 让 Unity 帮我们完成剩余阴影投射的部分,把结果输出到深度图和阴影纹理中。

TRANSFORM_SHADOW_CASTER_NORMALOFFSET 会使用名称为 v 作为输入结构体,v 中需要包含顶点位置 v.vertex 和 顶点法线 v.normal 信息。

本文使用 Zhihu On VSCode 创作并发布


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3